<!doctype html>
<html lang="zh-CN">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover">
  <title>Quiet Tap Relax</title>
  <style>
    :root{
      --bg:#0d1117;--panel:#161b22;--text:#c9d1d9;--muted:#8b949e;--accent:#58a6ff;--good:#9ef0a3;--warn:#f2cc60;--bad:#ff7b72;
      --glow:0 0 12px rgba(88,166,255,.25);
    }
    *{box-sizing:border-box;-webkit-tap-highlight-color:transparent}
    html,body{height:100%;margin:0;background:var(--bg);color:var(--text);font-family:ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial,"Noto Sans",sans-serif}
    #app{position:fixed;inset:0;display:flex;flex-direction:column}
    header{height:52px;display:flex;align-items:center;justify-content:space-between;padding:0 12px;background:linear-gradient(180deg,rgba(255,255,255,0.02),rgba(255,255,255,0));border-bottom:1px solid rgba(255,255,255,0.06)}
    .hud{display:flex;gap:12px;align-items:center;font-variant-numeric:tabular-nums}
    .chip{padding:6px 10px;background:var(--panel);border:1px solid rgba(255,255,255,0.06);border-radius:8px;box-shadow:var(--glow)}
    .chip b{color:var(--good)}
    .btns{display:flex;gap:8px}
    button{height:32px;padding:0 10px;background:var(--panel);color:var(--text);border:1px solid rgba(255,255,255,0.08);border-radius:8px;cursor:pointer}
    button:hover{border-color:rgba(88,166,255,.4)}
    button.primary{background:#1b2430;border-color:#2b3542;color:#dbe7ff}
    #stage{position:relative;flex:1;overflow:hidden}
    canvas{position:absolute;inset:0;width:100%;height:100%;display:block}
    .overlay{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;pointer-events:auto}
    [hidden]{display:none !important}
    .modal{pointer-events:auto;min-width:260px;max-width:min(92vw,420px);background:var(--panel);border:1px solid rgba(255,255,255,0.08);border-radius:12px;padding:16px;box-shadow:0 10px 30px rgba(0,0,0,.5)}
    .modal h1{font-size:18px;margin:0 0 8px}
    .modal p{margin:6px 0;color:var(--muted)}
    .row{display:flex;gap:8px;margin-top:12px}
    .score-large{font-size:28px;color:var(--accent)}
    .toast{position:absolute;left:50%;top:18%;transform:translateX(-50%);color:#cfe9ff;background:rgba(28,38,50,.6);border:1px solid rgba(88,166,255,.25);padding:6px 10px;border-radius:8px;opacity:0;transition:opacity .2s}
    .show{opacity:1}
  </style>
</head>
<body>
  <div id="app">
    <header>
      <div class="hud">
        <div class="chip">分数 <b id="score">0</b></div>
        <div class="chip">子弹 <b id="ammo">10</b></div>
        <div class="chip">倒计时 <b id="timer">60</b>s</div>
      </div>
      <div class="btns">
        <button id="btnStart" class="primary">开始</button>
        <button id="btnPause">暂停</button>
        <button id="btnMute">静音</button>
      </div>
    </header>
    <div id="stage">
      <canvas id="cv"></canvas>
      <div class="overlay" id="overlay" hidden>
        <div class="modal" id="modal">
          <h1 id="modalTitle">Quiet Tap Relax</h1>
          <p id="modalMsg">上班族低打扰解压 60s 模式（枪台底部居中，移动瞄准，点击发射。小目标可穿透、石头反弹一次、树干阻挡）</p>
          <p>最高分：<span id="best">0</span></p>
          <div class="row">
            <button id="btnAgain" class="primary">开始游戏</button>
            <button id="btnClose">关闭</button>
          </div>
        </div>
      </div>
      <div class="toast" id="toast"></div>
    </div>
  </div>

  <script>
  ;(() => {
    const cvs = document.getElementById('cv')
    const ctx = cvs.getContext('2d')
    const $ = (id)=>document.getElementById(id)
    const elScore=$('score'), elAmmo=$('ammo'), elTimer=$('timer')
    const overlay=$('overlay'), modal=$('modal'), bestEl=$('best'), modalTitle=$('modalTitle'), modalMsg=$('modalMsg')
    const toast=$('toast')

    const DPR = Math.max(1, Math.min(2, window.devicePixelRatio || 1))
    const GAME = {state:'init', time:60, score:0, ammo:10, muted:true, lastPointer:{x:0,y:0}, startedOnce:false, supplyStep:5, supplyPer:10, gun:{x:0,y:0,angle:0}}

    const GRID = {cols:4, rows:5, holes:[], popInterval:[600,1100], dwell:[2550,2900]}
    const TRUNKS = []
    const TYPES = {
      bird:  {color:'#7bc6ff', score:10,  weight:8,  radius:20, small:true},
      leaf:  {color:'#8fdc8f', score:-5,  weight:3,  radius:18, small:true},
      stone: {color:'#a0a9b0', score:-10, weight:2,  radius:22, small:false, stone:true},
      child: {color:'#ffd7a0', score:-100, weight:0.5, radius:22, small:false, protected:true}
    }

    const STORE_KEY = 'qtr_best'

    function resize(){
      const rect = cvs.getBoundingClientRect()
      cvs.width = Math.floor(rect.width * DPR)
      cvs.height = Math.floor(rect.height * DPR)
      ctx.setTransform(DPR,0,0,DPR,0,0)
      layoutGrid(rect.width, rect.height)
    }

    function layoutGrid(w,h){
      GRID.holes.length = 0; TRUNKS.length=0
      const pad = 24
      const areaW = w - pad*2
      const areaH = h - pad*2
      const cellW = areaW / GRID.cols
      const cellH = areaH / GRID.rows
      for(let r=0;r<GRID.rows;r++){
        for(let c=0;c<GRID.cols;c++){
          GRID.holes.push({
            x: pad + c*cellW + cellW/2,
            y: pad + r*cellH + cellH*0.62,
            active:false,
            type:null,
            t0:0,
            dwell:0
          })
        }
      }
      // trunks: add blockers
      const midY = h*0.45
      TRUNKS.push({x: w*0.28, y: midY, w: 18, h: h*0.6})
      TRUNKS.push({x: w*0.58, y: midY*0.95, w: 22, h: h*0.6})
      // gun position bottom-center
      GAME.gun.x = w/2; GAME.gun.y = h - 30
    }

    const rng = (a,b)=>a+Math.random()*(b-a)
    const now = ()=>performance.now()

    const targets = []
    const bullets = []
    function spawn(){
      if(GAME.state!=='playing') return
      const hole = pickHole()
      if(!hole || hole.active) return
      const type = pickType()
      hole.active = true
      hole.type = type
      hole.t0 = now()
      // 距离越远停留越久：按与枪台距离占屏幕对角线比例加成
      const rect = cvs.getBoundingClientRect()
      const dist = Math.hypot(hole.x - GAME.gun.x, hole.y - GAME.gun.y)
      const maxDist = Math.hypot(rect.width, rect.height)
      const ratio = Math.min(1, dist / maxDist)
      hole.dwell = rng(GRID.dwell[0], GRID.dwell[1]) + ratio * 2000
      targets.push(hole)
    }

    function pickHole(){
      const inactive = GRID.holes.filter(h=>!h.active)
      if(inactive.length===0) return null
      return inactive[(Math.random()*inactive.length)|0]
    }

    function pickType(){
      const entries = Object.entries(TYPES)
      const total = entries.reduce((s,[,t])=>s+(t.weight||1),0)
      let r = Math.random()*total
      for(const [k,t] of entries){
        r -= (t.weight||1)
        if(r<=0) return k
      }
      return 'bird'
    }

    function update(dt){
      if(GAME.state!=='playing') return
      // targets lifetime
      for(let i=targets.length-1;i>=0;i--){
        const h = targets[i]
        if(now()-h.t0>h.dwell){ h.active=false; h.type=null; targets.splice(i,1) }
      }
      // bullets movement & collisions
      for(let i=bullets.length-1;i>=0;i--){
        const b = bullets[i]; if(!b.alive){ bullets.splice(i,1); continue }
        b.x += b.vx*dt; b.y += b.vy*dt
        // trunk block
        for(const t of TRUNKS){
          if(b.x>=t.x-t.w/2 && b.x<=t.x+t.w/2 && b.y>=t.y-t.h/2 && b.y<=t.y+t.h/2){ b.alive=false; break }
        }
        if(!b.alive) continue
        // target hit
        let hitIdx=-1
        for(let j=targets.length-1;j>=0;j--){
          const h=targets[j]; const tp=TYPES[h.type]
          const p = 1 - (now()-h.t0)/h.dwell; const cy = h.y - 24*(1-p)
          const dx=b.x-h.x, dy=b.y-cy
          if(dx*dx+dy*dy <= (tp.radius+3)*(tp.radius+3)){ hitIdx=j; break }
        }
        if(hitIdx>=0){
          const h=targets[hitIdx]; const tp=TYPES[h.type]
          targets.splice(hitIdx,1); h.active=false; h.type=null
          if(tp.protected){ addScore(-100); endGame(); return }
          addScore(tp.score)
          if(tp.stone && !b.rebound){
            // random rebound direction, keep one more hit
            const ang = Math.random()*Math.PI*2; b.vx = Math.cos(ang)*1.2; b.vy = Math.sin(ang)*1.2; b.rebound=true; b.hitsLeft=1
          }else{
            if(tp.small && b.hitsLeft>1){ b.hitsLeft--; } else { b.alive=false }
          }
        }
        // out of bounds -> miss
        const W=cvs.width/DPR, H=cvs.height/DPR
        if(b.x<0||b.x>W||b.y<0||b.y>H){ if(b.alive){ addScore(-1); } b.alive=false }
      }
      // spawns
      popTimer -= dt
      if(popTimer<=0){
        spawn()
        const speedUp = GAME.time<30? -120: 0
        popTimer = rng(GRID.popInterval[0]+speedUp, GRID.popInterval[1]+speedUp)
      }
      // timer
      timeTimer -= dt
      if(timeTimer<=0){
        GAME.time = Math.max(0, GAME.time-1)
        elTimer.textContent = GAME.time
        timeTimer = 1000
        if(GAME.time===0) endGame()
      }
    }

    function draw(){
      const rect = cvs.getBoundingClientRect()
      ctx.clearRect(0,0,rect.width,rect.height)
      ctx.save()
      // background trunks
      ctx.fillStyle = 'rgba(255,255,255,0.06)'
      for(const t of TRUNKS){ ctx.fillRect(t.x - t.w/2, t.y - t.h/2, t.w, t.h) }
      // holes
      ctx.strokeStyle = 'rgba(255,255,255,0.06)'
      for(const h of GRID.holes){ ctx.beginPath(); ctx.arc(h.x, h.y, 22, 0, Math.PI*2); ctx.stroke() }
      // targets
      for(const h of targets){
        const t = TYPES[h.type]
        const p = 1 - (now()-h.t0)/h.dwell
        const y = h.y - 24*(1-p)
        ctx.beginPath(); ctx.fillStyle = t.color
        ctx.arc(h.x, y, t.radius, 0, Math.PI*2); ctx.fill()
        if(t.protected){ ctx.lineWidth=2; ctx.strokeStyle='rgba(255,215,160,.6)'; ctx.stroke() }
      }
      // bullets
      ctx.fillStyle = '#c9d1d9'
      for(const b of bullets){ ctx.beginPath(); ctx.arc(b.x, b.y, 3, 0, Math.PI*2); ctx.fill() }
      // gun
      ctx.strokeStyle='rgba(88,166,255,.5)'; ctx.lineWidth=2
      ctx.beginPath(); ctx.moveTo(GAME.gun.x, GAME.gun.y)
      const gx = GAME.gun.x + Math.cos(GAME.gun.angle)*40, gy = GAME.gun.y + Math.sin(GAME.gun.angle)*40
      ctx.lineTo(gx, gy); ctx.stroke()
      ctx.fillStyle = '#1b2430'; ctx.beginPath(); ctx.arc(GAME.gun.x, GAME.gun.y, 10, 0, Math.PI*2); ctx.fill()
      ctx.restore()
    }

    function fire(){
      if(GAME.state!=='playing') return
      if(GAME.ammo<=0){ endGame(); return }
      GAME.ammo--; elAmmo.textContent = GAME.ammo
      const b = {x: GAME.gun.x, y: GAME.gun.y, vx: Math.cos(GAME.gun.angle)*0.6, vy: Math.sin(GAME.gun.angle)*0.6, hitsLeft:3, rebound:false, alive:true}
      bullets.push(b)
    }

    function addScore(v){
      const prev = GAME.score
      GAME.score = Math.max(0, GAME.score + v)
      elScore.textContent = GAME.score
      const prevTicks = Math.floor(prev / GAME.supplyStep)
      const nowTicks = Math.floor(GAME.score / GAME.supplyStep)
      if(nowTicks>prevTicks){ const add = (nowTicks-prevTicks)*GAME.supplyPer; GAME.ammo += add; elAmmo.textContent = GAME.ammo; flashToast('补给 +'+add, 'good') }
    }

    function flashToast(text, type){
      toast.textContent=text
      toast.style.color = type==='good'? '#c8ffd0' : type==='bad'? '#ffd1cc' : '#eae2b3'
      toast.classList.remove('show'); void toast.offsetWidth; toast.classList.add('show')
      clearTimeout(flashToast._t); flashToast._t=setTimeout(()=>toast.classList.remove('show'), 450)
    }

    function startGame(){
      GAME.state='playing'; GAME.time=60; GAME.score=0; GAME.ammo=10; bullets.length=0; targets.length=0
      elScore.textContent=0; elAmmo.textContent=GAME.ammo; elTimer.textContent=60
      const rect=cvs.getBoundingClientRect(); const x=rect.width/2, y=rect.height*0.6
      GAME.gun.angle = Math.atan2(y-GAME.gun.y, x-GAME.gun.x)
      popTimer = 300; timeTimer = 1000
      overlay.hidden = true
      GAME.startedOnce = true
    }
    function pauseGame(){ if(GAME.state==='playing'){ GAME.state='paused'; overlay.hidden=false; modalTitle.textContent='已暂停'; modalMsg.textContent='按开始继续'; btnAgain.textContent='继续'; }}
    function resumeGame(){ if(GAME.state==='paused'){ GAME.state='playing'; overlay.hidden=true; }}
    function endGame(){
      GAME.state='ended'; overlay.hidden=false
      modalTitle.textContent='本局结束'
      modalMsg.innerHTML='本局得分：<span class="score-large">'+GAME.score+'</span>'
      btnAgain.textContent='再来一局'
      const best = Number(localStorage.getItem(STORE_KEY)||0)
      const next = Math.max(best, GAME.score)
      if(next!==best) localStorage.setItem(STORE_KEY, String(next))
      bestEl.textContent = String(next)
    }

    let rafId=0, last=0, popTimer=400, timeTimer=1000
    function loop(ts){
      if(!last) last=ts
      const dt = ts-last; last=ts
      if(GAME.state==='playing') update(dt)
      draw()
      rafId=requestAnimationFrame(loop)
    }

    // inputs
    cvs.addEventListener('pointermove', (e)=>{
      const rect=cvs.getBoundingClientRect(); const x=(e.clientX-rect.left), y=(e.clientY-rect.top)
      GAME.lastPointer={x,y}
      GAME.gun.angle = Math.atan2(y-GAME.gun.y, x-GAME.gun.x)
    })
    cvs.addEventListener('pointerdown', (e)=>{
      const rect=cvs.getBoundingClientRect(); const x=(e.clientX-rect.left), y=(e.clientY-rect.top)
      GAME.lastPointer={x,y}
      if(GAME.state==='init' || GAME.state==='ended'){ startGame(); return }
      if(GAME.state==='paused'){ resumeGame(); return }
      GAME.gun.angle = Math.atan2(y-GAME.gun.y, x-GAME.gun.x)
      fire()
    })
    window.addEventListener('keydown', (e)=>{
      if(e.code==='Space'){
        e.preventDefault()
        if(GAME.state==='init' || GAME.state==='ended'){ startGame(); return }
        if(GAME.state==='paused'){ resumeGame(); return }
        fire()
      }else if(e.key==='p' || e.key==='P'){ (GAME.state==='playing')?pauseGame():resumeGame() }
      else if(e.key==='r' || e.key==='R'){ startGame() }
      else if(e.key==='Escape'){ GAME.muted=!GAME.muted; btnMute.textContent = GAME.muted? '静音':'已开声' }
    }, {passive:false})

    // buttons
    const btnStart=$('btnStart'), btnPause=$('btnPause'), btnMute=$('btnMute'), btnAgain=$('btnAgain'), btnClose=$('btnClose')
    btnStart.addEventListener('click', ()=>{ if(GAME.state==='playing') return; startGame() })
    btnPause.addEventListener('click', ()=>{ (GAME.state==='playing')?pauseGame():resumeGame() })
    btnMute.addEventListener('click', ()=>{ GAME.muted=!GAME.muted; btnMute.textContent = GAME.muted? '静音':'已开声' })
    btnAgain.addEventListener('click', ()=> { startGame(); })
    btnClose.addEventListener('click', ()=>{ overlay.hidden=true; if(GAME.state==='paused') resumeGame() })

    // lifecycle
    window.addEventListener('resize', resize)
    window.addEventListener('blur', ()=>{ if(GAME.state==='playing'){ pauseGame() } })
    document.addEventListener('gesturestart', e=>e.preventDefault())
    document.addEventListener('touchmove', e=>{ if(e.target===cvs) e.preventDefault() }, {passive:false})

    // init
    resize(); bestEl.textContent = String(localStorage.getItem(STORE_KEY)||0)
    overlay.hidden = false
    modalTitle.textContent='Quiet Tap Relax'
    modalMsg.textContent='上班族低打扰解压 60s 模式（枪台底部居中，移动瞄准，点击发射。小目标可穿透、石头反弹一次、树干阻挡）'
    btnAgain.textContent='开始游戏'
    requestAnimationFrame(loop)
  })()
  </script>
</body>
</html>
